Skip to content

feat: initial ito-markets SDK for PyPI#1

Merged
affaan-m merged 2 commits into
mainfrom
devin/initial-sdk-setup
Jun 22, 2026
Merged

feat: initial ito-markets SDK for PyPI#1
affaan-m merged 2 commits into
mainfrom
devin/initial-sdk-setup

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Initial public release of the Ito Markets Python SDK. Distribution name ito-markets on PyPI (pip install ito-markets), import as from ito import ItoClient.

Included:

  • ItoClient with four resource namespaces: baskets (9 endpoints), markets (3), data (2), backtests (9)
  • HttpTransport with automatic retry + exponential backoff on 429/5xx
  • Typed exception hierarchy: ItoAPIError -> ItoAuthError, ItoRateLimitError, ItoNotFoundError, ItoValidationError
  • Context manager support, py.typed marker
  • 21 tests (respx mocks) covering all endpoints + error handling
  • CI workflow: pytest + ruff across Python 3.10/3.11/3.12
  • Publish workflow: GitHub Release -> PyPI via Trusted Publishers (OIDC, no API tokens)

Why ito-markets not ito: The ito name on PyPI is taken by an unrelated package (rexsutton/vollab). The Python import name remains ito.

To publish after merge: create a GitHub Release (e.g. v0.1.0) after configuring a Trusted Publisher on PyPI.

Link to Devin session: https://app.devin.ai/sessions/ef08e8d7517d4afcaf2492b519ab9a1a
Requested by: @alejandorosumah-mansa

Greptile Summary

This PR introduces the initial public release of the ito-markets Python SDK, providing a typed, synchronous client (ItoClient) over the Ito Markets REST API with automatic retry/backoff, a typed exception hierarchy, and 21 respx-mocked tests.

  • HttpTransport in src/ito/_http.py handles retries with exponential backoff on 429/5xx; 429 is included in RETRYABLE_STATUS_CODES but is dead code there since the explicit == 429 branch always executes first.
  • Backtests._build_spec uses falsy truthiness checks (if params:) rather than if params is not None:, silently dropping any explicitly-passed empty dict — a caller who passes params={} to explicitly clear defaults will have that key omitted from the request body.
  • Backtests.run() does a bare submission["data"]["run_id"] key access that will surface as an opaque KeyError if the API response doesn't match the expected shape.

Confidence Score: 4/5

Safe to merge with the _build_spec truthiness fix applied; the silent-drop of empty dicts is the only issue that can cause wrong API payloads.

The backtest spec builder drops any explicitly-passed empty dict (params, execution, budgets) because it uses if params: instead of if params is not None:, meaning callers who pass {} to deliberately omit a key or reset defaults will silently get the wrong request body. The rest of the SDK — transport, error handling, resource methods, CI/publish workflows — looks solid.

src/ito/resources/backtests.py (_build_spec truthiness checks and run() key access) and src/ito/_http.py (User-Agent version drift, dead 429 entry in RETRYABLE_STATUS_CODES).

Important Files Changed

Filename Overview
src/ito/resources/backtests.py Backtest resource with 9 endpoints — _build_spec uses falsy checks (if params:) instead of None-checks, silently dropping explicitly-passed empty dicts; run() also does an unguarded key access on submission["data"]["run_id"].
src/ito/_http.py HTTP transport with retry/backoff — 429 appears in both RETRYABLE_STATUS_CODES (dead entry) and its own explicit branch; User-Agent version is hardcoded and will drift from version.
src/ito/client.py Main ItoClient entry point — clean composition of transport and resource namespaces, context manager and close() correctly delegate to transport.
src/ito/exceptions.py Exception hierarchy — well-structured with ItoAPIError as base; ItoRateLimitError correctly adds retry_after attribute.
tests/test_client.py 21 tests covering all resources and error handling with respx mocks; fixture uses max_retries=0 which avoids sleep in tests. No test for _build_spec dropping empty-dict params.
.github/workflows/publish.yml Publish workflow uses OIDC Trusted Publishers with pypa/gh-action-pypi-publish, no secrets needed; build and publish jobs are correctly separated.
pyproject.toml Package metadata is complete and correct; httpx dependency pinned to >=0.25,<1.0 is appropriate for a library.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant User
    participant ItoClient
    participant HttpTransport
    participant ItoAPI

    User->>ItoClient: client.backtests.run(strategy_id, ...)
    ItoClient->>HttpTransport: post("/backtests/atop", body)
    loop Retry (up to max_retries)
        HttpTransport->>ItoAPI: POST /backtests/atop
        alt 429 Rate Limit
            ItoAPI-->>HttpTransport: 429 + Retry-After
            HttpTransport->>HttpTransport: sleep(retry_after or backoff)
        else 5xx Transient
            ItoAPI-->>HttpTransport: 500/502/503/504
            HttpTransport->>HttpTransport: sleep(backoff)
        else Network Error
            HttpTransport->>HttpTransport: catch TransportError, sleep(backoff)
        else Success
            ItoAPI-->>HttpTransport: "200 {data: {run_id}}"
            HttpTransport-->>ItoClient: dict
        end
    end
    loop Poll until done
        ItoClient->>HttpTransport: "get("/backtests/atop/{run_id}")"
        HttpTransport->>ItoAPI: "GET /backtests/atop/{run_id}"
        ItoAPI-->>HttpTransport: "200 {data: {status}}"
        HttpTransport-->>ItoClient: result dict
        alt "status == succeeded/failed/error"
            ItoClient-->>User: final result
        else still running
            ItoClient->>ItoClient: sleep(poll_interval)
        end
    end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant User
    participant ItoClient
    participant HttpTransport
    participant ItoAPI

    User->>ItoClient: client.backtests.run(strategy_id, ...)
    ItoClient->>HttpTransport: post("/backtests/atop", body)
    loop Retry (up to max_retries)
        HttpTransport->>ItoAPI: POST /backtests/atop
        alt 429 Rate Limit
            ItoAPI-->>HttpTransport: 429 + Retry-After
            HttpTransport->>HttpTransport: sleep(retry_after or backoff)
        else 5xx Transient
            ItoAPI-->>HttpTransport: 500/502/503/504
            HttpTransport->>HttpTransport: sleep(backoff)
        else Network Error
            HttpTransport->>HttpTransport: catch TransportError, sleep(backoff)
        else Success
            ItoAPI-->>HttpTransport: "200 {data: {run_id}}"
            HttpTransport-->>ItoClient: dict
        end
    end
    loop Poll until done
        ItoClient->>HttpTransport: "get("/backtests/atop/{run_id}")"
        HttpTransport->>ItoAPI: "GET /backtests/atop/{run_id}"
        ItoAPI-->>HttpTransport: "200 {data: {status}}"
        HttpTransport-->>ItoClient: result dict
        alt "status == succeeded/failed/error"
            ItoClient-->>User: final result
        else still running
            ItoClient->>ItoClient: sleep(poll_interval)
        end
    end
Loading

Comments Outside Diff (1)

  1. src/ito/resources/backtests.py, line 811 (link)

    P2 Bare key access on submission["data"]["run_id"] will raise an opaque KeyError if the API ever returns a successful 2xx with an unexpected body shape (e.g., during a schema change or when a validation message is returned without run_id). Consider falling back with a clearer error: run_id = submission.get("data", {}).get("run_id") followed by a check that run_id is truthy, raising ItoAPIError with a descriptive message if it isn't.

Reviews (1): Last reviewed commit: "Merge initial SDK setup into main" | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

alejandorosumah-mansa and others added 2 commits June 22, 2026 22:30
Typed Python client for the Ito Markets public API. Includes:
- Baskets, Markets, Data, and Backtests resource namespaces
- Automatic retry with exponential backoff on 429/5xx
- Typed exceptions (ItoAuthError, ItoRateLimitError, etc.)
- Context manager support
- CI workflow (pytest + ruff on Python 3.10-3.12)
- PyPI publish workflow via Trusted Publishers (OIDC)

Distribution name: ito-markets (pip install ito-markets)
Import name: ito (from ito import ItoClient)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@alejandorosumah-mansa alejandorosumah-mansa self-assigned this Jun 22, 2026
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@affaan-m affaan-m merged commit 8bb50a2 into main Jun 22, 2026
3 checks passed
Comment on lines +186 to +193
if params:
spec["params"] = params
if execution:
spec["execution"] = execution
if budgets:
spec["budgets"] = budgets
if basket_id:
spec["basket_id"] = basket_id

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Falsy check silently drops explicitly-passed empty dicts. A caller who intentionally passes params={}, execution={}, or budgets={} to override defaults (or to conform to an API schema that expects the key present) will have those values silently omitted from the request body. if params is not None: is the correct guard — it distinguishes "user explicitly passed an empty dict" from "user passed nothing".

Suggested change
if params:
spec["params"] = params
if execution:
spec["execution"] = execution
if budgets:
spec["budgets"] = budgets
if basket_id:
spec["basket_id"] = basket_id
if params is not None:
spec["params"] = params
if execution is not None:
spec["execution"] = execution
if budgets is not None:
spec["budgets"] = budgets
if basket_id is not None:
spec["basket_id"] = basket_id

Comment thread src/ito/_http.py
base_url=self._base_url,
headers={
"Authorization": f"Bearer {api_key}",
"User-Agent": "ito-python/0.1.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The version string in the User-Agent header is hardcoded to "ito-python/0.1.0" and will fall out of sync with __version__ in src/ito/__init__.py on every release. Import and use the package version instead.

Suggested change
"User-Agent": "ito-python/0.1.0",
"User-Agent": f"ito-python/{__import__('ito').__version__}",

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment thread src/ito/_http.py
DEFAULT_TIMEOUT = 30.0
MAX_RETRIES = 3
RETRY_BACKOFF_FACTOR = 1.0
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 429 is already a member of RETRYABLE_STATUS_CODES but is also handled by the dedicated if response.status_code == 429: branch above, making the set entry dead code. Including it here creates the false impression that the generic retry path handles rate limiting, when it never reaches that check for 429. Removing it from the set makes the intent explicit.

Suggested change
RETRYABLE_STATUS_CODES = {429, 500, 502, 503, 504}
RETRYABLE_STATUS_CODES = {500, 502, 503, 504}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants